/*
 ------------------------------------------------------------------------------
 Copyright (C) 2013 Eternal Games.

 This file is part of the EternalTech source code.

 The EternalTech source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The EternalTech source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the EternalTech source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


//
// sv_client.cpp - client interaction
//


#include "sv_local.h"


/*
 ==================
 
 Called when the client is totally leaving the server, either willingly or
 unwillingly. This is NOT called if the entire server is quiting or crashing.
 ==================
*/
void SV_DropClient (client_t *client, const char *reason){

	char	command[MAX_STRING_LENGTH];
	int		index;

	if (client->state <= CS_ZOMBIE)
		return;		// Already dropped

	// Abort a download if in progress

	// Disconnect the client from the game
	glqSGame->ClientDisconnect(client->clientNum);

	// Drop the client, it will become free in a few seconds
	Com_DPrintf("Going to CS_ZOMBIE for %s\n", client->playerName);

	client->state = CS_ZOMBIE;

	// Tell everyone why this client got dropped
	SV_BroadcastReliableCommand("print %s" S_COLOR_DEFAULT " disconnected - %s", client->playerName, reason);

	// If the reliable command buffer will overflow, send an out-of-band
	// disconnect packet instead
	if (client->reliableCommandSequence - client->reliableCommandAcknowledge == MAX_RELIABLE_COMMANDS){
		NET_OutOfBandPrintf(NS_SERVER, client->netChan.remoteAddress, "disconnect %s", reason);
		return;
	}

	// Send a reliable disconnect command
	Str_SPrintf(command, sizeof(command), "disconnect \"%s\"", reason);

	// Store it off
	client->reliableCommandSequence++;

	index = client->reliableCommandSequence & (MAX_RELIABLE_COMMANDS - 1);
	Str_Copy(client->reliableCommandList[index], command, sizeof(client->reliableCommandList[index]));
}

/*
 ==================
 SV_UserInfoChanged

 The user info string for a client has been changed, so parse new information
 out of it. This will happen when the client connects to the server, and
 possibly during gameplay.
 ==================
*/
void SV_UserInfoChanged (client_t *client){

	char	value[MAX_STRING_LENGTH];

	// Parse cl_playerName
	if (!Info_GetValueForKey(client->userInfo, "cl_playerName", value))
		Str_Copy(client->playerName, "Unnamed Player", sizeof(client->playerName));
	else
		Str_Copy(client->playerName, value, sizeof(client->playerName));

	// Parse cl_voiceChat
	if (!Info_GetValueForKey(client->userInfo, "cl_voiceChat", value))
		client->voiceChat = 3;
	else
		client->voiceChat = glqMath::ClampInt(Str_ToInteger(value), 0, 3);

	// Parse cl_rate
	if (NET_IsLANAddress(client->netChan.remoteAddress))
		client->rate = 250000;
	else {
		if (!Info_GetValueForKey(client->userInfo, "cl_rate", value))
			client->rate = 10000;
		else
			client->rate = glqMath::ClampInt(Str_ToInteger(value), 5000, 100000);
	}
}


/*
 ==============================================================================

 SERVER PACKET PARSING

 ==============================================================================
*/


/*
 ==================
 SV_GetChallengePacket

 Responds with a challenge number that can be used in a subsequent connect
 command.
 We do this to prevent denial of service attacks that flood the server with
 invalid connection IPs. With a challenge, they must give a valid IP address.
 ==================
*/
static void SV_GetChallengePacket (const netAdr_t from){

	challenge_t	*challenge;
	int			oldest, oldestTime;
	int			i;

	oldest = 0;
	oldestTime = 0x7FFFFFFF;

	// See if we already have a challenge for this IP
	for (i = 0, challenge = svs.challenges; i < MAX_CHALLENGES; i++, challenge++){
		if (NET_CompareAddress(from, challenge->address))
			break;		// Reuse this challenge

		// Overwrite the oldest
		if (challenge->time < oldestTime){
			oldest = i;
			oldestTime = challenge->time;
		}
	}

	if (i == MAX_CHALLENGES){
		// This is the first time this client has asked for a challenge
		challenge = &svs.challenges[oldest];

		challenge->address = from;
		challenge->challenge = ((svs.random.RandInt() << 16) ^ svs.random.RandInt()) ^ svs.time;
	}

	// Set the time
	challenge->time = svs.time;

	// Send the challenge response
	NET_OutOfBandPrintf(NS_SERVER, from, "challengeResponse %i", challenge->challenge);
}

/*
 ==================
 SV_ConnectPacket

 Connects the client to the server
 ==================
*/
static void SV_ConnectPacket (const netAdr_t from){

	client_t	*client;
	char		userInfo[MAX_STRING_LENGTH];
	char		password[MAX_STRING_LENGTH];
	const char	*reason;
	int			protocol, challenge, channelPort;
	int			ping;
	int			i;

	if (Cmd_Argc() != 5){
		Com_DPrintf("Rejected client connection from %s: bad connection request\n", NET_AddressToString(from));

		NET_OutOfBandPrintf(NS_SERVER, from, "connectResponse %s", "Bad connection request");
		return;
	}

	// Parse the arguments
	protocol = Str_ToInteger(Cmd_Argv(1));
	challenge = Str_ToInteger(Cmd_Argv(2));
	channelPort = Str_ToInteger(Cmd_Argv(3));

	Str_Copy(userInfo, Cmd_Argv(4), sizeof(userInfo));

	// Reject internet clients if we are a LAN server
	if (sv_lanServer->integerValue){
		if (!NET_IsLANAddress(from)){
			Com_DPrintf("Rejected client connection from %s: not from the local network\n", NET_AddressToString(from));

			NET_OutOfBandPrintf(NS_SERVER, from, "connectResponse %s", "Server is for LAN clients only");
			return;
		}
	}

	// Reject the connection if the client tried to reconnect too soon
	for (i = 0, client = svs.clients; i < sv_maxClients->integerValue; i++, client++){
		if (client->state == CS_FREE)
			continue;

		if (!NET_CompareBaseAddress(from, client->netChan.remoteAddress))
			continue;

		if (channelPort == client->netChan.channelPort || from.port == client->netChan.remoteAddress.port){
			if (svs.time - client->lastConnectTime < SEC2MS(sv_reconnectLimit->floatValue)){
				Com_DPrintf("Rejected client connection from %s: reconnected too soon\n", NET_AddressToString(from));

				NET_OutOfBandPrintf(NS_SERVER, from, "connectResponse %s", "Reconnected too soon");
				return;
			}

			break;
		}
	}

	// Make sure the protocol is right
	if (protocol != PROTOCOL_VERSION){
		Com_DPrintf("Rejected client connection from %s: wrong protocol version\n", NET_AddressToString(from));

		NET_OutOfBandPrintf(NS_SERVER, from, "connectResponse %s", "Wrong protocol version");
		return;
	}

	// Make sure the challenge is valid
	if (!NET_IsLocalAddress(from)){
		for (i = 0; i < MAX_CHALLENGES; i++){
			if (NET_CompareAddress(from, svs.challenges[i].address))
				break;
		}

		if (i == MAX_CHALLENGES){
			Com_DPrintf("Rejected client connection from %s: no challenge\n", NET_AddressToString(from));

			NET_OutOfBandPrintf(NS_SERVER, from, "connectResponse %s", "No challenge");
			return;
		}

		if (svs.challenges[i].challenge != challenge){
			Com_DPrintf("Rejected client connection from %s: invalid challenge\n", NET_AddressToString(from));

			NET_OutOfBandPrintf(NS_SERVER, from, "connectResponse %s", "Invalid challenge");
			return;
		}

		// Reject the connection based on ping
		ping = svs.time - svs.challenges[i].time;

		if (!NET_IsLANAddress(from)){
			if (sv_minPing->integerValue && ping < sv_minPing->integerValue){
				Com_DPrintf("Rejected client connection from %s: too low ping\n", NET_AddressToString(from));

				NET_OutOfBandPrintf(NS_SERVER, from, "connectResponse %s", "Server is for high pings only");

				// Clear the challenge
				Mem_Fill(&svs.challenges[i], 0, sizeof(challenge_t));

				return;
			}

			if (sv_maxPing->integerValue && ping > sv_maxPing->integerValue){
				Com_DPrintf("Rejected client connection from %s: too high ping\n", NET_AddressToString(from));

				NET_OutOfBandPrintf(NS_SERVER, from, "connectResponse %s", "Server is for low pings only");

				// Clear the challenge
				Mem_Fill(&svs.challenges[i], 0, sizeof(challenge_t));

				return;
			}
		}
	}

	// Check password if needed
	if (sv_needPassword->integerValue && sv_password->value[0]){
		if (!NET_IsLocalAddress(from)){
			if (!Info_GetValueForKey(userInfo, "cl_password", password) || !password[0]){
				Com_DPrintf("Rejected client connection from %s: no password\n", NET_AddressToString(from));

				NET_OutOfBandPrintf(NS_SERVER, from, "connectResponse %s", "Server is password protected");
				return;
			}

			if (Str_Compare(sv_password->value, password)){
				Com_DPrintf("Rejected client connection from %s: invalid password\n", NET_AddressToString(from));

				NET_OutOfBandPrintf(NS_SERVER, from, "connectResponse %s", "Invalid password");
				return;
			}
		}
	}

	// If there is already a client slot for this IP, reuse it
	for (i = 0, client = svs.clients; i < sv_maxClients->integerValue; i++, client++){
		if (client->state == CS_FREE)
			continue;

		if (!NET_CompareBaseAddress(from, client->netChan.remoteAddress))
			continue;

		if (channelPort == client->netChan.channelPort || from.port == client->netChan.remoteAddress.port){
			if (!NET_IsLocalAddress(from))
				Com_Printf("Client reconnecting from %s with %i challenge ping\n", NET_AddressToString(from), ping);

			break;
		}
	}

	// Otherwise find a free client slot
	if (i == sv_maxClients->integerValue){
		for (i = 0, client = svs.clients; i < sv_maxClients->integerValue; i++, client++){
			if (client->state == CS_FREE)
				break;
		}

		if (i == sv_maxClients->integerValue){
			if (NET_IsLocalAddress(from))
				Com_Error(false, "Server is full on local connect");

			Com_DPrintf("Rejected client connection from %s: no free client slots\n", NET_AddressToString(from));

			NET_OutOfBandPrintf(NS_SERVER, from, "connectResponse %s", "Server is full");
			return;
		}

		if (!NET_IsLocalAddress(from))
			Com_Printf("Client connecting from %s with %i challenge ping\n", NET_AddressToString(from), ping);
	}

	// Give the game a chance to reject the connection or modify the user info
	reason = glqSGame->ClientConnect(client - svs.clients, NET_IsLocalAddress(from), NET_AddressToString(from), userInfo, true);
	if (reason){
		Com_DPrintf("Rejected client connection from %s: rejected by game (%s)\n", NET_AddressToString(from), reason);

		NET_OutOfBandPrintf(NS_SERVER, from, "connectResponse %s", reason);
		return;
	}

	// Accept the new client.
	// This is the only place a client_t is ever initialized.
	Mem_Fill(client, 0, sizeof(client_t));

	client->clientNum = client - svs.clients;

	client->serverId = 0;

	client->pureAuthentic = false;

	client->serverStateMessage = -1;

	client->gameStateMessage = -1;
	client->gameStateOffset = -1;

	Str_Copy(client->userInfo, userInfo, sizeof(client->userInfo));

	SV_UserInfoChanged(client);

	client->nextPacketTime = svs.time;			// Send a packet immediately

	// Set up the network channel
	Com_DPrintf("Going to CS_CONNECTED for %s\n", client->playerName);

	client->state = CS_CONNECTED;

	NetChan_Setup(&client->netChan, NS_SERVER, from, channelPort);

	client->lastConnectTime = svs.time;			// Don't allow a reconnect for a few seconds
	client->lastReceivedTime = svs.time;		// Don't time-out

	client->timeOutCount = 0;

	// Send the connect response
	NET_OutOfBandPrintf(NS_SERVER, from, "connectResponse");

	// Update number of clients for server info
	CVar_SetInteger(sv_numClients, sv_numClients->integerValue + 1);

	// If this was the first client on the server, or the last client the
	// server can hold, send a heartbeat to the master server immediately
	if (sv_numClients->integerValue == 1 || sv_numClients->integerValue == sv_maxClients->integerValue)
		svs.masterHeartbeatTime = svs.time - HEARTBEAT_TIMEOUT;
}

/*
 ==================
 
 Executes a console command from a remote system
 ==================
*/
static void SV_RConPacket (const netAdr_t from){

}

/*
 ==================
 
 ==================
*/
static void SV_ConnectionlessPacket (const netAdr_t from, msg_t *msg){

	char		string[MAX_STRING_LENGTH];
	const char	*cmd;

	// Read the message
	MSG_ClearReadState(msg);

	MSG_ReadLong(msg);
	MSG_ReadString(msg, string, sizeof(string));

	// Tokenize the string
	Cmd_TokenizeString(string);

	if (!Cmd_Argc())
		return;		// No tokens

	cmd = Cmd_Argv(0);

	Com_DPrintf("Client out-of-band packet from %s: %s\n", NET_AddressToString(from), cmd);

	// Client requesting a challenge
	if (!Str_ICompare(cmd, "getChallenge")){
		SV_GetChallengePacket(from);
		return;
	}

	// Client requesting a connection
	if (!Str_ICompare(cmd, "connect")){
		SV_ConnectPacket(from);
		return;
	}

	// Client sending a remote console command
	if (!Str_ICompare(cmd, "rcon")){
		SV_RConPacket(from);
		return;
	}

	Com_DPrintf(S_COLOR_YELLOW "Bad client out-of-band packet from %s:\n" S_COLOR_YELLOW "%s\n", NET_AddressToString(from), string);
}

/*
 ==================
 SV_ParsePacket
 ==================
*/
static void SV_ParsePacket (const netAdr_t from, msg_t *msg){

	client_t	*client;
	int			channelPort;
	int			i;

	// Check for a connectionless packet
	if (*(int *)msg->data == OOB_SEQUENCE){
		SV_ConnectionlessPacket(from, msg);
		return;
	}

	if (msg->writeBytes < CLIENT_PACKET_HEADER){
		Com_DPrintf(S_COLOR_YELLOW "Runt client packet from %s\n", NET_AddressToString(from));
		return;
	}

	// Read the channel port from the message so we can fix up stupid address
	// translating routers
	MSG_ClearReadState(msg);

	MSG_ReadLong(msg);
	MSG_ReadShort(msg);
	channelPort = MSG_ReadWord(msg);

	// Check for packets from connected clients
	for (i = 0, client = svs.clients; i < sv_maxClients->integerValue; i++, client++){
		if (client->state == CS_FREE)
			continue;

		if (!NET_CompareBaseAddress(from, client->netChan.remoteAddress))
			continue;

		// It is possible to have multiple clients from a single IP address,
		// so they are differentiated by the channel port value
		if (channelPort != client->netChan.channelPort)
			continue;

		// The IP port can't be used to differentiate them, because some
		// address translating routers periodically change UDP port assignments
		if (from.port != client->netChan.remoteAddress.port){
			Com_DPrintf("Fixing up a translated port from %s\n", NET_AddressToString(from));

			client->netChan.remoteAddress.port = from.port;
		}

		// Packet from client
		if (!NetChan_Process(&client->netChan, svs.time, msg))
			return;		// Out of order, duplicated, etc

		// Zombie clients still need to process the message to make sure they
		// don't need to retransmit the final reliable message, but they don't
		// do any other processing
		if (client->state == CS_ZOMBIE)
			return;

		// Don't time-out
		client->lastReceivedTime = svs.time;

		// Parse the message
		SV_ParseClientMessage(client, msg);

		return;
	}

	// If we received a sequenced packet from an address we don't recognize,
	// send an out-of-band disconnect packet to it
	Com_DPrintf(S_COLOR_YELLOW "Sequenced client packet without connection from %s\n", NET_AddressToString(from));

	NET_OutOfBandPrintf(NS_SERVER, from, "disconnect");
}

/*
 ==================
 SV_ReadPackets
 ==================
*/
void SV_ReadPackets (bool dump){

	byte		data[MAX_MSG_SIZE];
	int			size;
	netAdr_t	from;
	msg_t		msg;

	// Initialize the message buffer
	MSG_Init(&msg, data, sizeof(data), false);

	// Read incoming network packets
	while (NET_GetPacket(NS_SERVER, &from, data, &size, sizeof(data))){
		MSG_SetWriteState(&msg, size << 3);

		if (dump){
			Com_DPrintf(S_COLOR_YELLOW "Dumping client packet from %s\n", NET_AddressToString(from));
			continue;
		}

		// Parse the packet
		SV_ParsePacket(from, &msg);
	}

	// Read incoming loopback packets
	while (NET_GetLoopPacket(NS_SERVER, &from, data, &size, sizeof(data))){
		MSG_SetWriteState(&msg, size << 3);

		if (dump){
			Com_DPrintf(S_COLOR_YELLOW "Dumping client packet from %s\n", NET_AddressToString(from));
			continue;
		}

		// Parse the packet
		SV_ParsePacket(from, &msg);
	}
}